﻿using System;
using System.Collections.Generic;
using System.IO;
using WCell.Constants.World;
using WCell.Util.Graphics;

namespace WCell.Collision
{
    public class WorldBuildings
    {
        public const string BuildingPath = "D:\\Work\\Collision\\WMOs";

        private const float RadiansPerDegree = 0.0174532925f;
        private const string fileType = "wmo";
        private static readonly Dictionary<string, bool> tileLoaded = new Dictionary<string, bool>();
        private static readonly Dictionary<string, bool> noTileExists = new Dictionary<string, bool>();
        private static readonly Dictionary<MapId, bool> mapExists = new Dictionary<MapId, bool>();
        private static readonly Dictionary<MapId, TreeReference<Building>> worldBuildings =
            new Dictionary<MapId, TreeReference<Building>>();


        public static float? GetWMOHeight(MapId map, Vector3 pos)
        {
            // Start at the character's head and send a ray pointed at the floor
            var startPos = pos + Vector3.Up*2.0f; // what should the scalar be?
            var endPos = startPos + Vector3.Down*2000.0f; // what should the scalar be?

            var buildings = GetPotentialColliders(map, ref startPos, ref endPos);
            if (buildings.Count == 0) return null;

            float tMax;
            var ray = CollisionHelper.CreateRay(startPos, endPos, out tMax);

            var dist = CollideWithRay(buildings, tMax, ray);
            if (dist == null) return null;

            return (startPos.Z + Vector3.Down.Z*dist.Value);
        }

        public static bool HasLOS(MapId map, Vector3 startPos, Vector3 endPos)
        {
            return (CheckCollision(map, startPos, endPos) == null);
        }

        public static float? CheckCollision(MapId map, Vector3 startPos, Vector3 endPos)
        {
            var buildings = GetPotentialColliders(map, ref startPos, ref endPos);

            // No buildings intersect this footprint
            if (buildings.Count == 0) return null;

            float tMax;
            var ray = CollisionHelper.CreateRay(startPos, endPos, out tMax);

            return CollideWithRay(buildings, tMax, ray);
        }

        private static float? CollideWithRay(IList<Building> buildings, float tMax, Ray ray)
        {
            float? result = tMax;
            for(var i = 0; i < buildings.Count; i++)
            {
                var newResult = buildings[i].IntersectsWith(ref ray, ref tMax);
                if (newResult == null) continue;

                result = Math.Min(result.Value, newResult.Value);
            }

            return result < tMax ? result : null;
        }

        #region Load Data
        internal static QuadTree<Building> GetBuildingTree(MapId map, TileCoord tileCoord)
        {
            if (!HasMap(map))
                return null;

            TreeReference<Building> tree;

            lock (worldBuildings)
            {
                // map has tree?
                if (worldBuildings.TryGetValue(map, out tree))
                {
                    if (tree.Tree != null)
                        return tree.Tree;
                }
                else
                {
                    // Create map tree
                    var box = RegionBoundaries.GetRegionBoundaries()[(int)map];
                    worldBuildings[map] = tree = new TreeReference<Building>(new QuadTree<Building>(box));
                }
            }

            lock (tree.LoadLock)
            {
                EnsureGroupLoaded(tree, map, tileCoord);
                return tree.Tree;
            }
        }

        private static List<Building> GetPotentialColliders(MapId map, ref Vector3 startPos, ref Vector3 endPos)
        {
            var startCoord = LocationHelper.GetTileXYForPos(startPos);
            var endCoord = LocationHelper.GetTileXYForPos(endPos);

            var tree = GetBuildingTree(map, startCoord);
            if (startCoord != endCoord)
            {
                tree = GetBuildingTree(map, endCoord);
            }

            var footPrint = MakeBounds(ref startPos, ref endPos);
            return tree.Query(footPrint);
        }

        private static bool EnsureGroupLoaded(TreeReference<Building> tree, MapId map, TileCoord tileCoord)
        {
            var result = true;

            var tile = new TileCoord {
                TileX = tileCoord.TileX,
                TileY = tileCoord.TileY
            };
            result = EnsureTileLoaded(map, tile, tree);

            tile.TileX = tileCoord.TileX - 1;
            result = result && EnsureTileLoaded(map, tile, tree);

            tile.TileX = tileCoord.TileX + 1;
            result = result && EnsureTileLoaded(map, tile, tree);

            tile.TileX = tileCoord.TileX;
            tile.TileY = tileCoord.TileY - 1;
            result = result && EnsureTileLoaded(map, tile, tree);

            tile.TileY = tileCoord.TileY + 1;
            result = result && EnsureTileLoaded(map, tile, tree);

            return result;
        }

        private static bool EnsureTileLoaded(MapId map, TileCoord tile, TreeReference<Building> tree)
        {
            if (IsTileLoaded(map, tile) || noTileExists.ContainsKey(GenerateKey(map, tile))) return true;

            if (!LoadTile(tree, map, tile))
            {
                noTileExists.Add(GenerateKey(map, tile), true);
                return false;
            }
            return true;
        }

        private static bool LoadTile(TreeReference<Building> tree, MapId mapId, TileCoord tileCoord)
        {
            var dir = Path.Combine(BuildingPath, ((int)mapId).ToString());
            if (!Directory.Exists(dir)) return false;

            var fileName = String.Format("{0}{1}.fub", tileCoord.TileX, tileCoord.TileY);
            var fullPath = Path.Combine(dir, fileName);
            if (!File.Exists(fullPath)) return false;

            return LoadTileBuildings(tree, fullPath);
        }

        private static bool HasMap(MapId mapId)
        {
            lock (mapExists)
            {
                if (!IsLoaded(mapId))
                {
                    var mapPath = Path.Combine(BuildingPath, ((uint)mapId).ToString());
                    var exists = Directory.Exists(mapPath);

                    mapExists[mapId] = exists;
                    return exists;
                }

                return mapExists[mapId];
            }
        }

        private static bool IsLoaded(MapId mapId)
        {
            return mapExists.ContainsKey(mapId);
        }

        private static bool IsTileLoaded(MapId map, TileCoord tileCoord)
        {
            return (tileLoaded.ContainsKey(GenerateKey(map, tileCoord)));
        }

        private static string GenerateKey(MapId map, TileCoord tileCoord)
        {
            return String.Format("{0}_{1}_{2}", (int)map, tileCoord.TileX, tileCoord.TileY);
        }

        private static BoundingBox MakeBounds(ref Vector3 startPos, ref Vector3 endPos)
        {
            var newMin = Vector3.Min(startPos, endPos);
            var newMax = Vector3.Max(startPos, endPos);

            return new BoundingBox(newMin, newMax);
        }
        
        private static bool LoadTileBuildings(TreeReference<Building> tree, string filePath)
        {
            using(var file = File.OpenRead(filePath))
            using(var br = new BinaryReader(file))
            {
                var key = br.ReadString();
                if (key != fileType)
                {
                    Console.WriteLine("Invalid file format, suckah!");
                }

                ReadBuildings(br, tree);
                
                br.Close();
            }


            return true;
        }

        private static void ReadBuildings(BinaryReader br, TreeReference<Building> tree)
        {
            var numBuildings = br.ReadInt32();
            for (var i = 0; i < numBuildings; i++)
            {
                var bounds = br.ReadBoundingBox();
                var invRot = br.ReadSingle();
                var matrix = Matrix.CreateRotationY(((-1.0f*(invRot - 90)))*RadiansPerDegree);
                var center = br.ReadVector3();

                BuildingGroup[] groups;
                var numGroups = br.ReadInt32();
                if (numGroups > 0)
                {
                    groups = new BuildingGroup[numGroups];
                    for (var j = 0; j < numGroups; j++)
                    {
                        groups[j] = ReadBuildingGroup(br);
                    }
                }
                else
                {
                    groups = null;
                }

                var building = new Building {
                    Bounds = bounds,
                    Center = center,
                    InverseRotation = matrix,
                    BuildingGroups = groups
                };

                tree.Tree.Insert(building);
            }
        }

        private static BuildingGroup ReadBuildingGroup(BinaryReader br)
        {
            var group = new BuildingGroup {
                Bounds = br.ReadBoundingBox()
            };

            Vector3[] vertices;
            var numVerts = br.ReadInt32();
            if (numVerts > 0)
            {
                vertices = new Vector3[numVerts];
                for (var j = 0; j < numVerts; j++)
                {
                    vertices[j] = br.ReadVector3();
                }
            }
            else
            {
                vertices = null;
            }
            group.Vertices = vertices;
            group.Tree = ReadBSPTree(br);

            return group;
        }

        private static BSPTree ReadBSPTree(BinaryReader br)
        {
            var rootId = br.ReadInt16();
            BSPNode[] nodes;

            var numNodes = br.ReadInt32();
            if (numNodes > 0)
            {
                nodes = new BSPNode[numNodes];
                for (var i = 0; i < numNodes; i++)
                {
                    nodes[i] = ReadBSPNode(br);
                }
            }
            else
            {
                nodes = null;
            }

            return new BSPTree(rootId, nodes);
        }

        private static BSPNode ReadBSPNode(BinaryReader br)
        {
            var node = new BSPNode
            {
                flags = (BSPNodeFlags)br.ReadByte(),
                negChild = br.ReadInt16(),
                posChild = br.ReadInt16(),
                planeDist = br.ReadSingle()
            };

            var numIndices = br.ReadInt32();
            if (numIndices > 0)
            {
                var indices = new Index3[numIndices];
                for (var i = 0; i < numIndices; i++)
                {
                    indices[i] = br.ReadIndex3();
                }
                node.TriIndices = indices;
            }
            else
            {
                node.TriIndices = null;
            }

            return node;
        }
        #endregion

    }
}
